<?php

/*
 * Copyright (c) 2012-2013 Aalto University and University of Helsinki
 * MIT License
 * see LICENSE.txt for more information
 */

class Concept extends VocabularyDataObject {

  private $foundby;
  private $graph;

  // concept properties that should not be shown to users
  private $DELETED_PROPERTIES = array(
    'skosext:broaderGeneric',		# these are remnants of bad modeling
    'skosext:broaderPartitive',		# 

    'skos:prefLabel',			# because it's displayed separately
    'skos:hiddenLabel',			# because it's supposed to be hidden

    'skos:topConceptOf',		# because it's too technical, not relevant for users
    'skos:inScheme',			# should be evident in any case
  );

  public function __construct($model, $vocab, $resource, $graph) {
    parent::__construct($model, $vocab, $resource);
    
    $this->graph = $graph;
  }

  public function getUri() {
    return $this->resource->getUri();
  }

  public function getForeignPrefLabels() {
    return $this->getForeignLabels('skos:prefLabel');
  }

  public function getForeignAltLabels() {
    return $this->getForeignLabels('skos:altLabel');
  }

  public function getLabel() {
    return $this->resource->label($this->lang) ? $this->resource->label($this->lang)->getValue() : null;
  }

  public function getVocab() {
    return $this->vocab ? $this->vocab->getId() : null;
  }

  public function setFoundBy($altlabel) {
    $this->foundby = $altlabel;
  }

  public function getFoundBy() {
    return $this->foundby;
  }
  
  public function getProperties() {
    $properties = array();

    $members_array = array();
    $long_uris = $this->resource->propertyUris();

    foreach ($long_uris as &$prop) {
      if (EasyRdf_Namespace::shorten($prop)) // shortening property labels if possible
        $prop = $sprop = EasyRdf_Namespace::shorten($prop);
      else
        $sprop = "<$prop>"; // EasyRdf requires full URIs to be in angle brackets

      // Iterating through every literal and adding these to the data object.
      foreach ($this->resource->allLiterals($sprop) as $val) {
        if ($val->getLang() == $this->lang || $val->getLang() === null) {
          $properties[$prop][] = new ConceptPropertyValue($prop, null, null, $val->getLang(), $val->getValue());
        }
      }

      // Iterating through every resource and adding these to the data object.
      foreach ($this->resource->allResources($sprop) as $val) {
        $label = null;
        $label_lang = null;
        $exvocab = null;

        if ($prop === 'skos:narrower') {
          $collections = $this->graph->resourcesMatching('skos:member', $val);
          if (sizeof($collections) > 0) { // if the narrower concept is part of some collection
            foreach($collections as $coll) {
              $members_array = array_merge($this->getCollectionMembers($coll, $this->resource), $members_array);
            }
            continue;
          }
        }
        if ($prop === 'skos:exactMatch' || $prop === 'skos:narrowMatch' || $prop === 'skos:broadMatch' || $prop === 'owl:sameAs' || $prop === 'skos:closeMatch' || $prop === 'rdf:type' ) {
          $exuri = $val->getUri();
          $exvoc = $this->model->guessVocabularyFromURI($exuri);
          if ($exvoc) {
            $label_lang = $exvoc->getDefaultLanguage();
            $label = $this->getExternalLabel($exvoc, $exuri, $label_lang);
            $exvocab = $exvoc->getId();
          }
          if (!$exvoc || !$label) {
            $label = $val->shorten() ? $val->shorten() : $exuri;
            $label_lang = $this->lang;
            $exvocab = null;
          }
        }
        $prop_info = $this->getPropertyParam($val, $prop);
        if ($prop_info['label'] == null) {
          $prop_info['label'] = $label;
          $prop_info['lang'] = $label_lang;
          $prop_info['exvocab'] = $exvocab;
        }
        if ($prop_info['label'] !== null) {
          $properties[$prop_info['prop']][] = new ConceptPropertyValue(
            $prop_info['prop'],
            $prop_info['concept_uri'],
            $prop_info['vocab'],
            $prop_info['lang'],
            $prop_info['label'],
            $prop_info['exvocab']
          );
        }
      }
    }

    // finding out if the concept is a member of some group
    $reverseResources = $this->graph->resourcesMatching('skos:member', $this->resource);
    if (isset($reverseResources)) {
      foreach ($reverseResources as $reverseResource) {
        $exvocab = $this->model->guessVocabularyFromURI($reverseResource->getUri())->getId();
        $reverseUri = $reverseResource->getUri(null);
        $label = $reverseResource->label($this->lang) ? $reverseResource->label($this->lang) : $reverseResource->label();
        $properties['onki:memberOf'][] = new ConceptPropertyValue('onki:memberOf', $reverseUri, $exvocab, $label->getLang(), $label);
      }
    }

    // if skos:narrower properties are actually groups we need to remove duplicates.
    foreach ($members_array as $topConcept) {
      $topProp = new ConceptPropertyValue('skos:narrower', $topConcept['parts'], $topConcept['vocab'], null, $topConcept['label'], $exvocab = null);
      $properties['skos:narrower'][] = $topProp;
      foreach ($topConcept['sub_members'] as $subMember) {
        $topProp->addSubMember('onki:sub', $subMember['label'], $subMember['parts'], $subMember['vocab'], null, $subMember['external']);
      }
    }
    
    // sorting the properties to a order preferred in the ONKI concept page.
    $properties = $this->arbitrarySort($properties);

    // clean up: remove unwanted properties
    foreach ($this->DELETED_PROPERTIES as $prop) {
      if (isset($properties[$prop]))
        unset($properties[$prop]);
    }
    
    $ret = array();
    foreach($properties as $prop => $values) {
      $propres = new EasyRdf_Resource($prop, $this->graph);
      $proplabel = $propres->label($this->lang); // current language
      if (!$proplabel) $proplabel = $propres->label(); // any language
      $propobj = new ConceptProperty($prop, $proplabel, $values);
      if ($propobj->getLabel()) // only display properties for which we have a label
        $ret[] = $propobj;
    }
    
    return $ret;
  }


  /**
   * Generates and makes a query into a external vocabulary for an exact
   * match for a particular concept.
   * @param Vocabulary $exvoc external vocabulary to query
   * @param string $exuri concept URI
   * @param string $lang language of label to query for
   * @return string label, or null if not found in vocabulary
   */
  private function getExternalLabel($exvoc, $exuri, $lang) {
    if ($exvoc) {
      $exsparql = $exvoc->getSparql();
      $results = $exsparql->queryLabel($exuri, $lang);
      return isset($results[$lang]) ? $results[$lang] : null;
    } else {
      return null;
    }
  }
                                                           

  private function getPropertyParam($val, $prop = null) {
    $ret = array();
    $ret['label'] = $val->label($this->lang) ? $val->label($this->lang)->getValue() : null;
    $ret['concept_uri'] = $val->getUri();
    $ret['vocab'] = $this->getVocab();
    $ret['prop'] = $prop;
    $ret['lang'] = $this->lang;
    $ret['exvocab'] = $ret['vocab'];
    return $ret;
  }

  private function getCollectionMembers($coll, $resource) {
    $members_array = Array();
    $coll_info = $this->getPropertyParam($coll);
    $external = false;
    if (strstr($coll_info['concept_uri'], 'http')) // for identifying concepts that are found with a uri not consistent with the current vocabulary
      $external = true;
    $members_array[$coll->getUri()] = array('type' => 'resource', 'label' => $coll_info['label'],
        'uri' => $coll_info['concept_uri'], 'vocab' => $coll_info['vocab'], 'parts' => $coll->getUri(), 'external' => $external);
    $narrowers = $resource->allResources('skos:narrower');
    foreach ($coll->allResources('skos:member') as $member) {
      foreach ($narrowers as $narrower) {
        if ($narrower->getUri() === $member->getUri()) { // found a narrower concept that is a member of this collection
          $narrow_info = $this->getPropertyParam($narrower);
          $external = false;
          if (strstr($narrow_info['concept_uri'], 'http')) // for identifying concepts that are found with a uri not consistent with the current vocabulary
            $external = true;
          if ($narrow_info['label'] == null) { // fixes json encoded unicode characters causing labels to disappear in afo
            $narrow_info['label'] = ('"' . $narrow_info['concept_uri'] . '"');
            $narrow_info['label'] = json_decode($narrow_info['label']);
            $narrow_info['concept_uri'] = $narrow_info['label'];
            $narrow_info['label'] = strtr($narrow_info['label'], '_', ' ');
          }
          $members_array[$coll->getUri()]['sub_members'][] = array('type' => 'resource', 'label' => $narrow_info['label'],
              'uri' => $narrow_info['concept_uri'], 'vocab' => $narrow_info['vocab'], 'parts' => $narrower->getUri(), 'external' => $external);
        }
      }
    }
    return $members_array;
  }

  /**$ret[$uri]['parent'] =
   * Sorting the result list to a arbitrary order defined below in mycompare()
   * @param unknown_type $sortable
   */
  private function arbitrarySort($sortable) {
    // sorting the result list to a arbitrary order defined below in mycompare()
    if ($sortable == null)
      return $sortable;
    uksort($sortable, array($this, 'mycompare'));
    foreach ($sortable as &$property_list)
      $property_list = $this->array_sorter($property_list, 'label');

    return $sortable;
  }

  /**
   * Compares the given objects and returns -1 or 1 depending which ought to be first.
   * $order defines the priorities of the different properties possible in the array.
   * @param string $a the first item to be compared
   * @param string $b the second item to be compared
   */
  private static function mycompare($a, $b) {
    if ($a == $b) {
      return 0;
    }
    $order = array("rdf:type", "skos:definition", "skos:note", "skos:broader", "skos:narrower", "skos:related", "skos:altLabel");
    $position = array_search($a, $order);
    $position2 = array_search($b, $order);

    //if both are in the $order, then sort according to their order in $order...
    if ($position2 !== false && $position !== false) {
      return ($position < $position2) ? -1 : 1;
    }
    //if only one is in $order, then sort to put the one in $order first...
    if ($position !== false) {
      return -1;
    }
    if ($position2 !== false) {
      return 1;
    }

    //if neither in $order, then a simple alphabetic sort...
    return ($a < $b) ? -1 : 1;
  }

  /**
   * Sorting function for sorting arrays by a named subarray value..
   * for example ['label'] in arbitrary sort.
   * @param array $array
   * @param type $on
   * @param enum $order PHP Sort options attribute eg. SORT_DESC
   * @return array which should now be sorted.
   */
  private function array_sorter($array, $on, $order = SORT_ASC) {
    $new_array = array();
    $sortable_array = array();

    if (count($array) > 0) {
      foreach ($array as $k => $v) {
        if (is_array($v)) {
          foreach ($v as $k2 => $v2) {
            if ($k2 == $on) {
              $sortable_array[$k] = strtolower($v2);
            }
          }
        } else {
          $sortable_array[$k] = $v;
        }
      }

      switch ($order) {
        case SORT_ASC:
          asort($sortable_array);
          break;
        case SORT_DESC:
          arsort($sortable_array);
          break;
      }

      foreach ($sortable_array as $k => $v) {
        $new_array[$k] = $array[$k];
      }
    }

    return $new_array;
  }

  private function getForeignLabels($prop) {
    $labels = array();
    foreach($this->resource->allLiterals($prop) as $lit) {
      if ($lit->getLang() != $this->lang)
        $labels[$lit->getLang()][] = $lit->getValue();
    }
    return $labels;
  }

}


class ConceptProperty {
  private $prop;
  private $label;
  private $values;

  public function __construct($prop, $label, $values) {
    $this->prop = $prop;
    $this->label = $label;
    $this->values = $values;
  }

  public function getLabel() {
    // first see if we have a translation
    $label = gettext($this->prop);
    if ($label != $this->prop) return $label;
    // if not, see if there was a label for the property in the graph
    if ($this->label) return $this->label;
    // when no label is found, don't show the property at all
    return null;
  }
  
  public function getDescription() {
    $helpprop = $this->prop . "_help";
    return gettext($helpprop); // can't use string constant, it'd be picked up by xgettext
  }
  
  public function getValues() {
    return $this->values;
  }
}


class ConceptPropertyValue {

  private $lang;
  private $exvocab;
  private $type;
  private $label;
  private $uri;
  private $vocab;
  private $submembers;

  public function __construct($prop, $uri, $vocab, $lang, $label, $exvocab = null) {
    $this->submembers = array();
    $this->lang = $lang;
    $this->exvocab = $exvocab;
    $this->type = $prop;
    $this->label = $label;
    $this->uri = $uri;
    $this->vocab = $vocab;
  }

  public function getLang() {
    return $this->lang;
  }

  public function getExVocab() {
    return $this->exvocab;
  }

  public function getType() {
    return $this->type;
  }

  public function getLabel() {
    return $this->label;
  }

  public function getUri() {
    return $this->uri;
  }

  public function getVocab() {
    return $this->vocab;
  }

  public function addSubMember($type, $label, $uri, $vocab, $lang, $exvocab = null) {
    $this->submembers[$label] = new ConceptPropertyValue($type, $uri, $vocab, $lang, $label, $exvocab = null);
    $this->sortSubMembers();
  }

  public function getSubMembers() {
    if (empty($this->submembers))
      return null;
    return $this->submembers;
  }

  public function sortSubMembers() {
    if (!empty($this->submembers))
      ksort($this->submembers);
  }

}

?>
